﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.Tree;

namespace Neovolve.Extensibility.ReSharper
{
    /// <summary>
    /// The <see cref="NodeAnalyser"/>
    /// class is used to analyse nodes in order to identify them as an alias type or a CLR type.
    /// </summary>
    internal static class NodeAnalyser
    {
        /// <summary>
        /// Stores the type mappings of alias type declarations.
        /// </summary>
        private static readonly AliasTypeMappingCollection AliasTypeMappings;

        /// <summary>
        /// Stores the regular expression used to identify cref attribute values in a string.
        /// </summary>
        private static readonly Regex crefExpression =
            new Regex(
                @"cref=(?:""(?<TypeName>[^""]+)""|'(?<TypeName>[^']+)'|(?<TypeName>[^\s/>]+)(?:\s|/?>))",
                RegexOptions.Compiled | RegexOptions.IgnoreCase);

        /// <summary>
        /// Initializes static members of the <see cref="NodeAnalyser"/> class.
        /// </summary>
        static NodeAnalyser()
        {
            AliasTypeMappings = new AliasTypeMappingCollection();

            AliasTypeMappings.Add(new AliasTypeMapping("object", "Object", false));
            AliasTypeMappings.Add(new AliasTypeMapping("char", "Char", false));
            AliasTypeMappings.Add(new AliasTypeMapping("string", "String", false));
            AliasTypeMappings.Add(new AliasTypeMapping("bool", "Boolean", false));
            AliasTypeMappings.Add(new AliasTypeMapping("byte", "Byte", true));
            AliasTypeMappings.Add(new AliasTypeMapping("short", "Int16", true));
            AliasTypeMappings.Add(new AliasTypeMapping("int", "Int32", true));
            AliasTypeMappings.Add(new AliasTypeMapping("long", "Int64", true));
            AliasTypeMappings.Add(new AliasTypeMapping("ushort", "UInt16", true));
            AliasTypeMappings.Add(new AliasTypeMapping("uint", "UInt32", true));
            AliasTypeMappings.Add(new AliasTypeMapping("ulong", "UInt64", true));
            AliasTypeMappings.Add(new AliasTypeMapping("double", "Double", true));
            AliasTypeMappings.Add(new AliasTypeMapping("decimal", "Decimal", true));
            AliasTypeMappings.Add(new AliasTypeMapping("float", "Single", true));
            AliasTypeMappings.Add(new AliasTypeMapping("sbyte", "SByte", true));
        }

        /// <summary>
        /// Gets the cref captures.
        /// </summary>
        /// <param name="node">The node to search.</param>
        /// <returns>
        /// A <see cref="IEnumerable{Capture}"/> instance.
        /// </returns>
        public static IEnumerable<Capture> GetCrefCaptures(IDocCommentNode node)
        {
            MatchCollection matches = crefExpression.Matches(node.CommentText);

            // Check if there are any matches in this comment
            if (matches.Count > 0)
            {
                // Loop through each match
                for (Int32 matchIndex = 0; matchIndex < matches.Count; matchIndex++)
                {
                    Match match = matches[matchIndex];
                    Group group = match.Groups["TypeName"];

                    // Loop through each capture in the group
                    for (Int32 captureIndex = 0; captureIndex < group.Captures.Count; captureIndex++)
                    {
                        Capture capture = group.Captures[captureIndex];

                        // Return the capture
                        yield return capture;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the format type suggestion.
        /// </summary>
        /// <param name="typeName">Name of the type.</param>
        /// <param name="option">The option.</param>
        /// <returns>
        /// A <see cref="TypeAliasFormat"/> instance.
        /// </returns>
        public static TypeAliasFormat GetFormatTypeSuggestion(String typeName, TypeAliasFormat option)
        {
            const String Expression = @"\[[0-9]*\]$";
            String baseTypeName = Regex.Replace(typeName, Expression, String.Empty);

            return CalculateFormatTypeSuggestion(option, baseTypeName);
        }

        /// <summary>
        /// Gets the format type suggestion.
        /// </summary>
        /// <param name="node">The node to check.</param>
        /// <param name="option">The option.</param>
        /// <returns>A <see cref="TypeAliasFormat"/> value.</returns>
        public static TypeAliasFormat GetFormatTypeSuggestion(ITreeNode node, TypeAliasFormat option)
        {
            // Check for type usage nodes
            if (node is IDeclaredTypeUsageNode || node is ITypeUsageNode || node is IReferenceExpressionNode
                || node is IPredefinedTypeExpressionNode)
            {
                String typeName = node.GetText();
                TypeAliasFormat suggestion = GetFormatTypeSuggestion(typeName, option);

                if (suggestion == TypeAliasFormat.ConvertFromAlias)
                {
                    // Check that this node is not the base type of an enum declaration
                    if (node.Parent is IEnumBaseNode)
                    {
                        return TypeAliasFormat.Ignore;
                    }
                }

                return suggestion;
            }

            if (node is IDocCommentNode)
            {
                IDocCommentNode commentNode = node as IDocCommentNode;

                foreach (TypeAliasFormat suggestion in GetCrefSuggestions(commentNode, option))
                {
                    // Check if there is a conversion value
                    if (suggestion != TypeAliasFormat.Ignore)
                    {
                        // Return the suggestion
                        return suggestion;
                    }
                }
            }

            return TypeAliasFormat.Ignore;
        }

        /// <summary>
        /// Gets the new name of the type.
        /// </summary>
        /// <param name="currentTypeName">Name of the current type.</param>
        /// <param name="suggestion">The suggestion.</param>
        /// <returns>A <see cref="string"/> value.</returns>
        public static String GetNewTypeName(String currentTypeName, TypeAliasFormat suggestion)
        {
            const String Expression = @"\[[0-9]*\]$";
            String baseTypeName = Regex.Replace(currentTypeName, Expression, String.Empty);
            String newTypeName = baseTypeName;
            AliasTypeMapping mapping = AliasTypeMappings.FindByTypeName(baseTypeName);

            if (suggestion == TypeAliasFormat.ConvertFromAlias)
            {
                newTypeName = mapping.CLRTypeName;
            }
            else if (suggestion == TypeAliasFormat.ConvertToAlias)
            {
                newTypeName = mapping.AliasTypeName;
            }

            // Check if there is an array specifier
            if (Regex.IsMatch(currentTypeName, Expression))
            {
                // Add the array specifier back into the type
                String suffix = currentTypeName.Substring(baseTypeName.Length);

                newTypeName += suffix;
            }

            // Return the new type
            return newTypeName;
        }

        /// <summary>
        /// Calculates the format type suggestion.
        /// </summary>
        /// <param name="option">The option.</param>
        /// <param name="typeName">Name of the type.</param>
        /// <returns>
        /// A <see cref="TypeAliasFormat"/> instance.
        /// </returns>
        private static TypeAliasFormat CalculateFormatTypeSuggestion(TypeAliasFormat option, String typeName)
        {
            // Get the mapping for this type
            AliasTypeMapping mapping = AliasTypeMappings.FindByTypeName(typeName);

            // Check if no mapping was found
            if (mapping == null)
            {
                return TypeAliasFormat.Ignore;
            }

            // Alias types are stored as keys in the alias type mapping dictionary
            TypeResolution resolution = GetTypeResolution(mapping, typeName);

            // Check if the type is an alias type
            if (option == TypeAliasFormat.ConvertFromAlias
                && resolution == TypeResolution.AliasType)
            {
                return TypeAliasFormat.ConvertFromAlias;
            }

            // Full types are stored as values in the alias type mapping dictionary

            // Check if the type is a full type
            if (option == TypeAliasFormat.ConvertToAlias
                && resolution == TypeResolution.CLRType)
            {
                return TypeAliasFormat.ConvertToAlias;
            }

            return TypeAliasFormat.Ignore;
        }

        /// <summary>
        /// Gets the cref suggestsions.
        /// </summary>
        /// <param name="node">The node to search.</param>
        /// <param name="option">The option.</param>
        /// <returns>
        /// A <see cref="IEnumerable&lt;TypeAliasFormat&gt;"/> instance.
        /// </returns>
        private static IEnumerable<TypeAliasFormat> GetCrefSuggestions(
            IDocCommentNode node, TypeAliasFormat option)
        {
            foreach (Capture capture in GetCrefCaptures(node))
            {
                String crefText = capture.Value;

                CrefAttributeDescriptor descriptor = new CrefAttributeDescriptor(crefText);
                ReadOnlyCollection<String> definedTypes = descriptor.TypeReferences;

                if (definedTypes.Count == 0)
                {
                    continue;
                }

                // Check if this is a generic type
                for (Int32 typeIndex = 0; typeIndex < definedTypes.Count; typeIndex++)
                {
                    String typeName = definedTypes[typeIndex];
                    TypeAliasFormat suggestion = CalculateFormatTypeSuggestion(option, typeName);

                    // Check if there is a conversion value
                    if (suggestion != TypeAliasFormat.Ignore)
                    {
                        // Return the suggestion
                        yield return suggestion;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the type resolution.
        /// </summary>
        /// <param name="mapping">The mapping.</param>
        /// <param name="typeName">Name of the type.</param>
        /// <returns>A <see cref="TypeResolution"/> value.</returns>
        private static TypeResolution GetTypeResolution(AliasTypeMapping mapping, String typeName)
        {
            // Check if we have found that the type is a CLR type
            if (mapping.CLRTypeName == typeName)
            {
                return TypeResolution.CLRType;
            }

            // Check if we have found that the type is an alias type
            if (mapping.AliasTypeName == typeName)
            {
                return TypeResolution.AliasType;
            }

            // The type wasn't valid for type mapping
            return TypeResolution.None;
        }
    }
}